
local os = os
local alien = require 'alien'
local struct = require 'alien.struct'
local win32 = require 'bindings.win32'
local lfs = require 'lfs'
local bit = require 'bit'
local print, error, require, pcall, tostring, type, pairs, collectgarbage, assert, ipairs
    = print, error, require, pcall, tostring, type, pairs, collectgarbage, assert, ipairs
local debug = require 'debug'
local max, floor, huge
    = math.max, math.floor, math.huge
local tconcat
    = table.concat
local strsub
    = string.sub
local keyboard = require 'guac.keyboard'


module (...)


for k,v in pairs(win32) do
  if (type(v) == 'number') and (type(k) == 'string') and (strsub(k,1,3) == 'VK_') then
    keyboard[strsub(k,4)] = v
  end
end
keyboard.CTRL = win32.VK_CONTROL
keyboard.ALT = win32.VK_MENU
keyboard.ESC = win32.VK_ESCAPE

local main_window

function get_main_hwnd()
  return main_window
end

function close()
  if (not main_window) or (win32.DestroyWindow(main_window) == 0) then
    return false
  end
  main_window = nil
  return true
end

-- guac module functions imported when window.open() is called
local declareKeyDown
local declareKeyUp
local refreshKeyboard
local declareMousePosition
local declareMouseButtonDown
local declareMouseButtonUp
local refreshMouse

function desktopDimensions()
  return win32.GetSystemMetrics(win32.SM_CXSCREEN), win32.GetSystemMetrics(win32.SM_CYSCREEN)
end

local WindowTitle = ""

function title(new)
  if new then
    WindowTitle = new
    if main_window and not in_fullscreen then
      win32.SetWindowTextA(main_window, tostring(WindowTitle))
    end
  else
    return WindowTitle
  end
end

local timerCallbacks = {}

function addInterval(func, ms)
  local callback = alien.callback(function()  return func();  end, win32.TimerProc_types)
  local timerID = win32.SetTimer(main_window, 0, ms, callback)
  timerCallbacks[timerID] = callback
  return timerID
end

function clearTimer(timerID)
  local result = win32.KillTimer(main_window, timerID)
  if (result == 1) then
    timerCallbacks[timerID] = nil
    return true
  else
    return false, win32.LastErrorMessage()
  end
end

local _onclose
function onclose(func)
  if func then
    assert(type(func)=='function' or not func, "onclose must be a function or nil")
    _onclose = func
  elseif _onclose then
    _onclose()
  end
end

local screen

local windowOpen = false

local mouseOffsetX, mouseOffsetY = 0, 0
local mouseScaleX, mouseScaleY = 1, 1
local mouseMaxX, mouseMaxY = huge, huge

function declareMouseOffset(new_x, new_y)
  mouseOffsetX, mouseOffsetY = new_x, new_y
end
function declareMouseScale(new_x, new_y)
  mouseScaleX, mouseScaleY = new_x, new_y
end
function declareMouseMax(new_x, new_y)
  mouseMaxX, mouseMaxY = new_x, new_y
end

do
  local ps = win32.PAINTSTRUCT:new()
  local tme = win32.TRACKMOUSEEVENT:new()
  tme.cbSize = win32.TRACKMOUSEEVENT.size
  tme.dwFlags = win32.TME_LEAVE
  
  local function WindowProcFactory(messageHandlers)
    return alien.callback(function(hwnd, uMsg, wParam, lParam)
      local handler = messageHandlers[uMsg]
      if handler then
        return handler(hwnd, uMsg, wParam, lParam)
      end
      return win32.DefWindowProcA(hwnd, uMsg, wParam, lParam)
    end, win32.WindowProc_types)
  end
  
  local mouseover = false
  local visiblecursor = true
  
  local function hidemouse()
    if visiblecursor then
      win32.ShowCursor(0)
      visiblecursor = false
    end
  end
  
  local function showmouse()
    if not visiblecursor then
      win32.ShowCursor(1)
      visiblecursor = true
    end
  end
  
  Main_WindowProc = WindowProcFactory{
    [win32.WM_CREATE] = function()
      windowOpen = true
    end;
    [win32.WM_DESTROY] = function()
      windowOpen = false
    end;
    [win32.WM_CLOSE] = function()
      if (_onclose == nil) or _onclose() then
        win32.DestroyWindow(main_window)
      end
      return 0
    end;
    [win32.WM_KEYDOWN] = function(hwnd, uMsg, wParam, lParam)
      declareKeyDown(wParam)
      return 0
    end;
    [win32.WM_KEYUP] = function(hwnd, uMsg, wParam, lParam)
      declareKeyUp(wParam)
      return 0
    end;
    [win32.WM_NCLBUTTONDBLCLK] = function(hwnd, uMsg, wParam, lParam)
      -- snap to shrinkwrap screen
      --[[
      if (wParam >= win32.HTSIZEFIRST) and (wParam <= win32.HTSIZELAST) then
        local windowRect, clientRect = win32.RECT:new(), win32.RECT:new()
        win32.GetWindowRect(hwnd, windowRect())
        win32.GetClientRect(hwnd, clientRect())
        if (wParam ~= win32.HTTOP) and (wParam ~= win32.HTBOTTOM) then
          w = (windowRect.right - windowRect.left) - clientRect.right + 320*screenScale
        else
          w = windowRect.right - windowRect.left
        end
        if (wParam ~= win32.HTLEFT) and (wParam ~= win32.HTRIGHT) then
          h = (windowRect.bottom - windowRect.top) - clientRect.bottom + 200*screenScale
        else
          h = windowRect.bottom - windowRect.top
        end
        local x, y
        if (wParam == win32.HTLEFT) or (wParam == win32.HTTOPLEFT) or (wParam == win32.HTBOTTOMLEFT) then
          x = windowRect.right - w
        else
          x = windowRect.left
        end
        if (wParam == win32.HTTOP) or (wParam == win32.HTTOPLEFT) or (wParam == win32.HTTOPRIGHT) then
          y = windowRect.bottom - h
        else
          y = windowRect.top
        end
        
        local boxed_ptr = alien.buffer(alien.sizeof('long'))
        boxed_ptr:set(1, lParam, 'long')
      
        win32.SetWindowPos(main_window, nil, x, y, w, h,
          win32.SWP_NOZORDER + win32.SWP_NOACTIVATE)
        return 0
      end
      --]]
      return win32.DefWindowProcA(hwnd, uMsg, wParam, lParam)
    end;
    [win32.WM_NCMOUSEMOVE] = function(hwnd, uMsg, wParam, lParam)
      if mouseover then
        mouseover = false
        declareMousePosition(nil, nil)
        showmouse()
      end
      return win32.DefWindowProcA(hwnd, uMsg, wParam, lParam)
    end;
    [win32.WM_PAINT] = function(hwnd, uMsg, wParam, lParam)
      screen.redraw()
      win32.ValidateRect(hwnd, nil)
      return 0
    end;
    [win32.WM_GETMINMAXINFO] = function(hwnd, uMsg, wParam, lParam)
      local windowRect = win32.RECT:new()
      win32.GetWindowRect(hwnd, windowRect())
      if (windowRect.bottom > 0) then
        local clientRect = win32.RECT:new()
        win32.assertNonZero(win32.GetClientRect(hwnd, clientRect()))
        minWidth = (windowRect.right - windowRect.left) - clientRect.right + 320
        minHeight = (windowRect.bottom - windowRect.top) - clientRect.bottom + 200
        local boxed_ptr = alien.buffer(alien.sizeof('long'))
        boxed_ptr:set(1, lParam, 'long')
        local maxinfo = win32.MINMAXINFO:new(boxed_ptr:get(1 + alien.sizeof('long') - alien.sizeof('pointer'), 'pointer'))
        maxinfo.ptMinTrackSize_x = minWidth
        maxinfo.ptMinTrackSize_y = minHeight
      end
      return 0
    end;
    [win32.WM_MOUSEMOVE] = function(hwnd, uMsg, wParam, lParam)
      if not mouseover then
        mouseover = true
        tme.hwndTrack = hwnd
        win32.TrackMouseEvent(tme())
      end
      local mouseX, mouseY = bit.band(lParam, 0xFFFF), bit.rshift(bit.band(lParam, 0xFFFF0000), 16)
      mouseX, mouseY = (mouseX + mouseOffsetX) * mouseScaleX, (mouseY + mouseOffsetY) * mouseScaleY
      if (mouseX < 0) or (mouseY < 0) or (mouseX >= mouseMaxX) or (mouseY >= mouseMaxY) then
        mouseX, mouseY = nil, nil
        showmouse()
      else
        mouseX, mouseY = floor(mouseX), floor(mouseY)
        hidemouse()
      end
      declareMousePosition(mouseX, mouseY)
      return 0
    end;
    [win32.WM_MOUSELEAVE] = function(hwnd, uMsg, wParam, lParam)
      if mouseover then
        mouseover = false
        declareMousePosition(nil, nil)
        showmouse()
      end
      return win32.DefWindowProcA(hwnd, uMsg, wParam, lParam)
    end;
    [win32.WM_LBUTTONDOWN] = function()
      declareMouseButtonDown(1)
      return 0
    end;
    [win32.WM_LBUTTONUP] = function()
      declareMouseButtonUp(1)
      return 0
    end;
    [win32.WM_RBUTTONDOWN] = function()
      declareMouseButtonUp(2)
      return 0
    end;
    [win32.WM_ACTIVATE] = function(hwnd, uMsg, wParam, lParam)
      if (wParam == 0) then
        declareMousePosition(nil, nil)
        showmouse()
      end
    end;
    [win32.WM_RBUTTONUP] = function()
      declareMouseButtonUp(2)
      return 0
    end;
    [win32.WM_MBUTTONDOWN] = function()
      declareMouseButtonUp(3)
      return 0
    end;
    [win32.WM_MBUTTONUP] = function()
      declareMouseButtonUp(3)
      return 0
    end;
    [win32.WM_ERASEBKGND] = function()
      return 1
    end;
    [win32.WM_SIZE] = function(hwnd, uMsg, wParam, lParam)
      local w, h = bit.band(lParam, 0xFFFF), bit.rshift(lParam, 16)
      screen.declareWindowSize(w, h)
      screen.redraw()
      return 0
    end;
  }
end

local msg = win32.MSG:new()

local ClassName = "Guac_MainWindow"

local use_opengl = false

function init_opengl()
  use_opengl = true
end

  
function open()

  declareKeyUp = keyboard.declareKeyUp
  declareKeyDown = keyboard.declareKeyDown
  refreshKeyboard = keyboard.refresh
  
  local mouse = require 'guac.mouse'
  declareMousePosition = mouse.declarePosition
  declareMouseButtonDown = mouse.declareButtonDown
  declareMouseButtonUp = mouse.declareButtonUp
  refreshMouse = mouse.refresh

  screen = require 'guac.screen'
  screen.init()
  
  local classdef = win32.WNDCLASSEX:new()

  classdef.cbSize = win32.WNDCLASSEX.size
  classdef.style = win32.CS_VREDRAW + win32.CS_HREDRAW + win32.CS_OWNDC
  classdef.lpfnWndProc = Main_WindowProc
  classdef.cbClsExtra = 0
  classdef.cbWndExtra = 0
  classdef.hInstance = nil
  classdef.hIcon = nil
  classdef.hCursor = win32.LoadCursorA(nil, win32.IDC_ARROW)
  classdef.hbrBackground = win32.GetStockObject(win32.NULL_BRUSH)
  classdef.lpszMenuName = nil
  classdef.lpszClassName = ClassName
  classdef.hIconSm = nil
  
  win32.RegisterClassExA(classdef())
  
  --
  local x, y, w, h
  
  local style, exStyle
  
  if fullscreen then
    x, y = 0, 0
    w, h = desktopDimensions()
    style, exStyle = win32.WS_POPUP, win32.WS_EX_TOPMOST
    in_fullscreen = true
  else
    x, y = win32.CW_USEDEFAULT, win32.CW_USEDEFAULT
    w, h = win32.CW_USEDEFAULT, win32.CW_USEDEFAULT
    style, exStyle = win32.WS_OVERLAPPEDWINDOW + win32.WS_CLIPCHILDREN, 0
    in_fullscreen = false
  end
  
  main_window = win32.assert(win32.CreateWindowExA(
    exStyle,
    ClassName,
    WindowTitle,
    style,
    x,
    y,
    w,
    h,
    nil,
    nil,
    nil,
    nil))
  
  local rect = win32.RECT:new()
  
  win32.GetClientRect(main_window, rect())
  
  if not fullscreen then
    local windowRect = win32.RECT:new()
    win32.GetWindowRect(main_window, windowRect())
    
    local offX = (windowRect.right - windowRect.left) - rect.right
    local offY = (windowRect.right - windowRect.left) - rect.right
    
    w, h = offX + 320, offY + 200
    win32.SetWindowPos(main_window, nil, 0, 0, w, h, win32.SWP_NOZORDER + win32.SWP_NOMOVE + win32.SWP_NOACTIVATE)
  end
  
  screen.init()
  
  if use_opengl then
    local gl = require 'bindings.gl'
    
    local dc = win32.GetDC(main_window)
    
    local pfd = win32.PIXELFORMATDESCRIPTOR:new() do
      pfd.nSize = win32.PIXELFORMATDESCRIPTOR.size
      pfd.nVersion = 1
      pfd.dwFlags = win32.PFD_DRAW_TO_WINDOW + win32.PFD_SUPPORT_OPENGL + win32.PFD_DOUBLEBUFFER
      pfd.cColorBits = 24
      pfd.cDepthBits = 16
      pfd.iLayerType = win32.PFD_MAIN_PLANE
    end
    
    local iFormat = win32.ChoosePixelFormat(dc, pfd())
    win32.SetPixelFormat(dc, iFormat, pfd())
    
    local rc = gl.wglCreateContext(dc)
    screen.declareRedrawFunc(function()
      gl.wglMakeCurrent(dc, rc)
      screen.internal_render()
      win32.SwapBuffers(dc)
    end)
  end
  
  --render.init_hwnd(inner_window)
  --render.to_hwnd_dc(inner_window, win32.GetDC(inner_window))
  
  win32.ShowWindow(main_window, win32.SW_SHOWNORMAL)
  win32.UpdateWindow(main_window)
end

local mousemove
function onmousemove(func)
  mousemove = func
end

do
  local msg = win32.MSG:new()
  function loop()
    while windowOpen and (win32.GetMessageA(msg(), nil, 0, 0) ~= 0) do
      win32.TranslateMessage(msg())
      win32.DispatchMessageA(msg())
    end
  end
end
